[FE302] React 基礎 - hooks 版本:state


Posted by s103071049 on 2021-09-06

初探 state

畫面永遠由 state 產生,UI 只是 state 的 mapping。ie UI = f(state),只要關注畫面長甚麼樣子就可以了。

使用 state 的方式

  1. import React from 'react'
  2. React.useState() 會回傳一個陣列,透過解構語法使我們不用 renaming
function useState() {
  return [123, 456]
}
const [a, b] = useState()

// 通常會用解構的寫法表示 
import {useState} from 'react'
const [todos, setTodos] = useState([
    123, 456, 555 // todos 的初始值
  ])

useState is a hook,透過這樣的方式在 function component 裡面使用 state。

function App() {
  const [counter, setCounter] = React.useState(0) // 初始值是 0
  const handleButtonClick = () => {
    setCounter(counter + 1)
  } 
  return (
    <div className="App">
      counter: {counter}
      <button onClick={handleButtonClick}>increment</button>
      <TodoItem content={21}/>
      <BlackTodoItem content={212} size="XL" />
    </div>
  );
}

React 裡面有個動作是 render,意思是他執行 app 這個 function, react 會將 return 的東西放到畫面上,放到畫面上這個動作是 mount,就是把東西放到 dom 上去。mount 完後再更新 state,他就會 re-render,re-render 就是再呼叫一次 app 這個 function 的意思。

加上 console 看得更清楚

function App() {
  const [counter, setCounter] = React.useState(0) // 初始值是 0
  console.log('render', counter)
  const handleButtonClick = () => {
    console.log('button click')
    setCounter(counter + 1)
  } 
  return (
    <div className="App">
      counter: {counter}
      <button onClick={handleButtonClick}>increment</button>
      <TodoItem content={21}/>
      <BlackTodoItem content={212} size="XL" />
    </div>
  );
}

jsx 裡沒有迴圈,所以只能透過 functional 的方式做,

render 一系列資料:透過 map 的方式將 array 裡的每個東西都 map 成一個 component,他就會變成很多 component 的意思。

兩個寫法等價

  const [todos, setTodos] = React.useState([
    123, 456, 555
  ])

  {// render 多個 component 可以傳陣列
    [<TodoItem content={21}/>, <TodoItem content={212}/>]
  }

  { 
    todos.map(todo => <TodoItem content={todo} />)
  }

console 出現錯誤訊息 : Warning: Each child in a list should have a unique "key" prop.

render 一個陣列要幫他加上一個 key,key 會幫 react 辨別他是哪一個 item。一般來說不推薦用 index 當 key,但我們暫時先用 index 當 key。

  {
    todos.map((todo, index) => <TodoItem key={index} content={todo} />)
  }

注意一:改 state 要用 setter 的方式改,不是用 push 方式亂改。

注意二:因為 .push 會改變原本的 todos,所以 todos.push(123) 後我的 todos 變成 [123, 456, 555, 123],而 setTodos(todos) 的 todos 也是 [123, 456, 555, 123],當 react 判定舊的 state 與新的 state 一樣,他就部會做事情。因為更新成一樣的 state 就等於不更新。

  const [todos, setTodos] = React.useState([
    123, 456, 555
  ]) 
  const handleButtonClick = () => {
    todos.push(123)
    setTodos(todos) // 裡面傳新的 todos 
  }

state is immutable in react,所以對於新增來說你會用解構語法這麼寫

前面是將 todos 的值複製過來,後面是新增的東西。

  const handleButtonClick = () => {
    setTodos([...todos,  Math.random()]) // 裡面傳新的 todos 
  }

總結

  1. 用 useState 後面傳狀態的初始值。使用前需要先 import {useState} from 'react'
  2. useState 會回給你一個陣列,第一個參數是 state 的值、第二個參數是 setter function 透過他去 set 你的 state
  3. 改變 state 時 call setter function,裡面傳新的 state。
  4. react state is immutable,也就是 state 基本上不變,不變指的是不能直接改他,ex:todos[1] = 100。所以要改 state 時要產生出一個新的 state 出來,怎麼產生會用一些慣用的 pattern。

再探 state 之新增 todo

jsx 裡面沒有內容一定要加上 slash,< />

加上 todo 功能

function App() {
  const [todos, setTodos] = useState([
    123, 456, 555
  ]) // 初始值是 0
  const handleButtonClick = () => {
    setTodos([...todos, Math.random()]) // 裡面傳新的 todos 
  } 
  return (
    <div className="App">
      <input type="text" placeholder="todo"/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo, index) => <TodoItem key={index} content={todo} />)
      }
    </div>
  );
}

react component 有兩種類型,以有無被 react 控制進行劃分。

一、controller component
二、uncontrollered component

一、controller component

在 react 裡面幾乎所有會動的東西都有它的狀態,當我在畫面上打字時,打的字也會在 react 的 state 裡。所以這個東西也要我自己進行控制。

  1. const [value, setValue] = useState('')
  2. 將 input 的值跟 state 做掛勾 <input type="text" placeholder="todo" value={value}/>
  3. 不管怎麼打字,畫面都不會變,因為我的 state 沒有改變。可以在 input 上聽一個 onChange 事件,命名慣例會用 handle 開頭,<input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
  const handleInputChange = (e) => {
    console.log(e.target.value) // 會將打的字的值給印出來,所以
    setValue(e.target.value)
  }

成功地順利打字!!可喜可賀 ~

  1. 第一次 render 時,value 是空字串
  2. 當我按 a 他觸發 handleInputChange 這個事件,他的 e.target.value 是 a 所以 setValue(e.target.value) === setValue(a),我的 value 就是 a
  3. state 改變就會重新 render,所以 re-render 的時候 input 的 value 變成 a
  4. 當我再打 b,setValue(e.target.value) === setValue(ab),所以 const [value, setValue] 的 value 就是 ab
  5. state 改變就會重新 render,所以 re-render 的時候 input 的 value 變成 ab,<input type="text" placeholder="todo" value={ab} onChange={handleInputChange}/>

透過這樣的操作,就可以將 input 的 state 的值給放到 state 裡面去。這是方法一、controller component,我的value 是有放在 state 裡面

二、uncontrollered component

  1. <input type="text" placeholder="todo"/>
  2. 拿到 input value 的方式:一、使用 className,透過 document.querySelector('.input-todo').value 拿值;二、透過 ref 存取 input dom 的元素,需要 import ref 的 hook:import {useState, useRef} from 'react'<input ref={inputRef} className="input-todo" type="text" placeholder="todo"/>
const inputRef = useRef()
const handleButtonClick = () => {
console.log(inputRef) // inputRef 是 obj 會有 current 這個 key
console.log(inputRef.current) // 他的 dom 元素
console.log(inputRef.current.value) // 輸入的東西
setTodos([...todos, Math.random()])
}

小結

input 的值有放到 state 裡面就是 controlled,沒有就是 uncontrolled
參考:官方文件 forms 裡面 controlled components,官方文件都是提供 class component 的用法,但可以透過 google 關鍵字 controlled components hooks / function components

controlled components 的方式完成新增 todo-list

import './App.css';
import styled from 'styled-components'
import {useState} from 'react'
import TodoItem from './TodoItem';

function App() {
  const [todos, setTodos] = useState([
    123
  ]) // 初始值是 0
  const [value, setValue] = useState('')
  const handleButtonClick = () => {
    setTodos([value, ...todos])
    setValue('') // 將 todo 清空
  }
  const handleInputChange = (e) => {
    setValue(e.target.value)
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo, index) => <TodoItem key={index} content={todo} />)
      }
    </div>
  );
}
export default App;

刪除、編輯有 todo 的 id 會比較好做事。此外也要儲存 todo 的其他狀態,

現在加上 id 的功能

import './App.css';
import styled from 'styled-components'
import {useState} from 'react'
import TodoItem from './TodoItem';
let id = 2 // 每次 render 都會重新呼叫 App 所以 id 要放外面
function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const handleButtonClick = () => {
    setTodos([{
      id, content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    id ++
  }
  const handleInputChange = (e) => {
    setValue(e.target.value)
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} content={todo.content} />)
      }
    </div>
  );
}
export default App;

將 id 用 useState 的方式表達也可以。但因為 id 沒有在畫面上出現,所以 id 改變不用重新渲染。react 的原則是 state 改變就會重新渲染,所以我們會不希望存成 state。

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const [id, SetId] = useState(2)
  const handleButtonClick = () => {
    setTodos([{
      id, content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    //id ++
    SetId(id + 1)
  }

我們也可以用 userRef 的方式寫,const id = useRef(2) // 他的初始值,useRef 比較神奇的點在為了讓值給以保存,在 component re-render 不會變,除了可以當 state 用也可以直接操作,他會維持原本的東西。使用 useRef 他會回傳一個物件,裡面的 current 值是你設定的初始值。

之所以會有 current 是因為物件指向的問題,

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const id = useRef(2) // 他的初始值
  const handleButtonClick = () => {
    setTodos([{
      id: id.current, 
      content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    //id ++
    id.current ++
  }
  下面略

為了確定有無成功,可以這麼做,todos.map((todo) => <TodoItem key={todo.id} todo={todo} />),將整個 todo 傳到 TodoItem,

function TodoItem ({className, size, todo}) {
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>completed</Button>
          <GreenButton>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

react 裡面使用表單相關,就要處理事件,

加上刪除 todo

App 是父親它 render TodoItem 小孩,所以我要怎麼在 TodoItem 改到 App ?

把這個 function 當作 props 傳給 TodoItem。將要做事情的 function 寫在 parent 然後傳給 child,child 再呼叫這個 function 就可以在 parent 這邊處理這個資訊。

// TodoItem
function TodoItem ({className, size, todo, handleDeleteTodo}) {
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>completed</Button>
          <GreenButton onClick= {() => {
            handleDeleteTodo(todo.id)
          }}>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

刪除不會改原本的陣列,會產生新的陣列。用 filter

// App
上略
  const handleDeleteTodo = id => {
    setTodos(todos.filter(todo => todo.id !== id)) // todo.id 不等於要刪除的 id 就會留下來
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo}/>)
      }
    </div>
  );
}

加上編輯 todo 功能

先檢視資料結構:現在只有 id, content

  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])

再加上 isDone,然後我們先調整介面

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'done', isDone: true},
    {id:2, content: 'not done', isDone: false}
  ])
  const [value, setValue] = useState('')
  const id = useRef(3) // 他的初始值

對 TodoItem 來說如果已完成他的 isDone 是 true,這時就要顯示未完成,反之 fase 已完成。

jsx 沒有 if-else,所以可以用短路或三元運算子去寫。然後加上刪除線的功能。

傳進去的 props 除了會給 styled-component 外,也會架在 dom 上去,傳的變數前面加上 $ 字號,她就不會被傳到 dom 上去。參考:transient props

const TodoContent = styled.div`
  color: ${props => props.theme.colors.red_300};
  font-size: 24px;
  ${props => props.size === 'XL' && `font-size: 20px;`};
  ${props => props.$isDone && `text-decoration: line-through`}
`
        <TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>
            {todo.isDone ? '未完成' : '已完成'}
          </Button>

處理 mark 的部份,用 map
基本上新增用解構的語法、刪除用 filter、修改用 map ,這是固定用法

// App
  const hadnleToggleIsDone = id => {
    setTodos(todos.map(todo => {
      if (todo.id !== id) return todo // 這個 id 不是我要修改的 id 我就將原本的 todo 回傳
      return {
        ...todo, // todo 原本的東西
        isDone: !todo.isDone // 我要修改的屬性
      }
    }))
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} hadnleToggleIsDone={hadnleToggleIsDone}/>)
      }
    </div>
  );

將它拆出來,可讀性會比 inline function 高很多。

function TodoItem ({className, size, todo, handleDeleteTodo, hadnleToggleIsDone}) {
  const handleToggleClick = () => {
    hadnleToggleIsDone(todo.id)
  }
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button onClick={handleToggleClick}>
            {todo.isDone ? '未完成' : '已完成'}
          </Button>
          <GreenButton onClick= {() => {
            handleDeleteTodo(todo.id)
          }}>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

todo list 中場總結

  1. Component (類似切版)
  2. Props (Component 可以傳自訂的 Props 進去,像是自訂的 html 元素,就可以在子層接收到這些東西)
  3. Style (css / inline-style / styled-component 寫法)
  4. Event Handler (加上 onClick / onSubmit / onMouseDown)
  5. JSX (render 簡單的 component、傳 js 用 { } 沒有迴圈跟 if-else,所以要用短路跟三元運算子寫、render 一系列 list 會用 map 變成陣列,只是 render 陣列要提供 key)
  6. State (用 useState 放初始值、用 setState 改變 state、immutable 的概念)

新增時會用一個新的陣列
刪除用 filter 產生新的陣列
編輯用 map 產生新的陣列










Related Posts

Markdown 功能筆記

Markdown 功能筆記

運用 Cli 部署 Vue專案 到 GitHub Pages

運用 Cli 部署 Vue專案 到 GitHub Pages

 [Node.js] call back queue運作機制

[Node.js] call back queue運作機制


Comments